/*******************************************************************************
 * Copyright (c) 2000, 2003 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
module dwt.internal.image.tifffileformat;


private import dwt.dwt;


private import dwt.graphics.imagedata;
private import dwt.graphics.imageloader;
private import dwt.graphics.imageloaderevent;
private import dwt.graphics.palettedata;
private import dwt.graphics.rgb;
private import dwt.internal.image.fileformat;
private import dwt.internal.image.ledatainputstream;
private import dwt.internal.image.ledataoutputstream;
private import dwt.util.javatypes;
private import dwt.util.util;

alias Exception StreamException;


class TIFFDirectory {

	TIFFRandomFileAccess file;
	boolean isLittleEndian;
	ImageLoader loader;
	int depth;
	
	/* Directory fields */
	int imageWidth;
	int imageLength;
	int[] bitsPerSample;
	int compression;
	int photometricInterpretation;
	int[] stripOffsets;
	int samplesPerPixel;
	int rowsPerStrip;
	int[] stripByteCounts;
	int t4Options;
	int colorMapOffset;
	
	/* Encoder fields */
	ImageData image;
	LEDataOutputStream out_renamed;
	
	static int NO_VALUE = -1;
	
	static const short TAG_ImageWidth = 256;
	static const short TAG_ImageLength = 257;
	static const short TAG_BitsPerSample = 258;
	static const short TAG_Compression = 259;
	static const short TAG_PhotometricInterpretation = 262;
	static const short TAG_StripOffsets = 273;
	static const short TAG_SamplesPerPixel = 277;
	static const short TAG_RowsPerStrip = 278;
	static const short TAG_StripByteCounts = 279;
	static const short TAG_XResolution = 282;
	static const short TAG_YResolution = 283;
	static const short TAG_T4Options = 292;
	static const short TAG_ResolutionUnit = 296;
	static const short TAG_ColorMap = 320;
	
	static const int TYPE_BYTE = 1;
	static const int TYPE_ASCII = 2;
	static const int TYPE_SHORT = 3;
	static const int TYPE_LONG = 4;
	static const int TYPE_RATIONAL = 5;
	
	/* Different compression schemes */
	static const int COMPRESSION_NONE = 1;
	static const int COMPRESSION_CCITT_3_1 = 2;
	static const int COMPRESSION_PACKBITS = 32773;
	
	static const int IFD_ENTRY_SIZE = 12;
	
public this(TIFFRandomFileAccess file, boolean isLittleEndian, ImageLoader loader) {
	this.file = file;
	this.isLittleEndian = isLittleEndian;
	this.loader = loader;
}

public this(ImageData image) {
	this.image = image;
}

/* PackBits decoder */
int decodePackBits(byte[] src, byte[] dest, int offsetDest) {
	int destIndex = offsetDest;
	int srcIndex = 0;
	while (srcIndex < src.length) {
		byte n = src[srcIndex];
		if (0 <= n && n <= 127) {
			/* Copy next n+1 bytes literally */
			System.arraycopy(src, ++srcIndex, dest, destIndex, n + 1);
			srcIndex += n + 1;		
			destIndex += n + 1;	
		} else if (-127 <= n && n <= -1) {
			/* Copy next ubyte -n+1 times */
			byte value = src[++srcIndex];
			for (int j = 0; j < -n + 1; j++) {
				dest[destIndex++] = value;
			}
			srcIndex++;
		} else {
			/* Noop when n == -128 */
			srcIndex++;
		}
	}
	/* Number of bytes copied */
	return destIndex - offsetDest;
}

int getEntryValue(int type, byte[] buffer, int index) {
	return toInt(buffer, index + 8, type);
}

void getEntryValue(int type, byte[] buffer, int index, int[] values) { // throws IOException {
	int start = index + 8;
	int size;
	int offset = toInt(buffer, start, TYPE_LONG);
	switch (type) {
		case TYPE_SHORT: size = 2; break;
		case TYPE_LONG: size = 4; break; 
		case TYPE_RATIONAL: size = 8; break;
		case TYPE_ASCII:
		case TYPE_BYTE: size = 1; break;
		default: DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT); return;
	}
	if (values.length * size > 4) {
		buffer = new byte[values.length * size]; 
		file.seek(offset);
		file.read(buffer);
		start = 0;
	}
	for (int i = 0; i < values.length; i++) {
		values[i] = toInt(buffer, start + i * size, type);
	}
}

void decodePixels(ImageData image) { // throws IOException {
	/* Each row is ubyte align_renameded */ 
	byte[] imageData = new byte[(imageWidth * depth + 7) / 8 * imageLength];
	image.data = imageData;
	int destIndex = 0;
	int length = stripOffsets.length;
	for (int i = 0; i < length; i++) {
		/* Read a strip */
		byte[] data = new byte[(stripByteCounts[i])];
		file.seek(stripOffsets[i]);
		file.read(data);
		if (compression == COMPRESSION_NONE) {
			System.arraycopy(data, 0, imageData, destIndex, data.length);
			destIndex += data.length;
		} else if (compression == COMPRESSION_PACKBITS) {
			destIndex += decodePackBits(data, imageData, destIndex);
		} else if (compression == COMPRESSION_CCITT_3_1 || compression == 3) {
			TIFFModifiedHuffmanCodec codec = new TIFFModifiedHuffmanCodec();
			int nRows = rowsPerStrip;
			if (i == length -1) {
				int n = imageLength % rowsPerStrip;
				if (n != 0) nRows = n;
			}
			destIndex += codec.decode(data, imageData, destIndex, imageWidth, nRows);
		}
		if (loader.hasListeners()) {
			loader.notifyListeners(new ImageLoaderEvent(loader, image, i, i == length - 1));
		}
	}
}

PaletteData getColorMap() { // throws IOException {
	int numColors = 1 << bitsPerSample[0];
	/* R, G, B entries are 16 bit wide (2 bytes) */
	int numBytes = 3 * 2 * numColors;
	byte[] buffer = new byte[numBytes];
	file.seek(colorMapOffset);
	file.read(buffer);
	DWTRGB[] colors = new DWTRGB[numColors];
	/**
	 * DWT does not support 16-bit depth color formats.
	 * Convert the 16-bit data to 8-bit data.
	 * The correct way to do this is to multiply each
	 * 16 bit value by the value:
	 * (2^8 - 1) / (2^16 - 1).
	 * The fast way to do this is just to drop the low
	 * ubyte of the 16-bit value.
	 */
	int offset = isLittleEndian ? 1 : 0;
	int startG = 2 * numColors;
	int startB = startG + 2 * numColors;
	for (int i = 0; i < numColors; i++) {
		int r = buffer[offset] & 0xFF;
		int g = buffer[startG + offset] & 0xFF;
		int b = buffer[startB + offset] & 0xFF;
		colors[i] = new DWTRGB(r, g, b);
		offset += 2;
	}
	return new PaletteData(colors);
}

PaletteData getGrayPalette() {
	int numColors = 1 << bitsPerSample[0];
	DWTRGB[] rgbs = new DWTRGB[numColors];
	for (int i = 0; i < numColors; i++) {
		int value = i * 0xFF / (numColors - 1);
		if (photometricInterpretation == 0) value = 0xFF - value;
		rgbs[i] = new DWTRGB(value, value, value);
	}
	return new PaletteData(rgbs);
}

PaletteData getRGBPalette(int bitsR, int bitsG, int bitsB) {	
	int blueMask = 0;
	for (int i = 0; i < bitsB; i++) {
		blueMask |= 1 << i;
	}
	int greenMask = 0;
	for (int i = bitsB; i < bitsB + bitsG; i++) {
		greenMask |= 1 << i;
	}
	int redMask = 0;
	for (int i = bitsB + bitsG; i < bitsB + bitsG + bitsR; i++) {
		redMask |= 1 << i;
	}	
	return new PaletteData(redMask, greenMask, blueMask);
}

int formatStrips(int rowByteSize, int nbrRows, byte[] data, int maxStripByteSize, int offsetPostIFD, int extraBytes, int[][] strips) {
	/* 
	* Calculate the nbr of required strips given the following requirements: 
	* - each strip should, if possible, not be greater than maxStripByteSize
	* - each strip should contain 1 or more entire rows
	* 
	* Format the strip fields arrays so that the image data is stored in one
	* contiguous block. This block is stored after the IFD and after any tag
	* info described in the IFD.
	*/
	int n, nbrRowsPerStrip;
	if (rowByteSize > maxStripByteSize) {
		/* Each strip contains 1 row */
		n = data.length / rowByteSize;
		nbrRowsPerStrip = 1;
	} else {
		int nbr = (data.length + maxStripByteSize - 1) / maxStripByteSize;
		nbrRowsPerStrip = nbrRows / nbr;
		n = (nbrRows + nbrRowsPerStrip - 1) / nbrRowsPerStrip;	
	}
	int stripByteSize = rowByteSize * nbrRowsPerStrip;

	int[] offsets = new int[n];
	int[] counts = new int[n];
	/* 
	* Nbr of bytes between the end of the IFD directory and the start of
	* the image data. Keep space for at least the offsets and counts
	* data, each field being TYPE_LONG (4 bytes). If other tags require
	* space between the IFD and the image block, use the extraBytes
	* parameter.
	* If there is only one strip, the offsets and counts data is stored
	* directly in the IFD and we need not reserve space for it.
	*/
	int postIFDData = n == 1 ? 0 : n * 2 * 4;
	int startOffset = offsetPostIFD + extraBytes + postIFDData; /* offset of image data */
	
	int offset = startOffset;
	for (int i = 0; i < n; i++) {
		/* 
		* Store all strips sequentially to allow us
		* to copy all pixels in one contiguous area.
		*/
		offsets[i] = offset;
		counts[i] = stripByteSize;
		offset += stripByteSize;
	}
	/* The last strip may contain fewer rows */
	int mod = data.length % stripByteSize;
	if (mod != 0) counts[counts.length - 1] = mod;
	
	strips[0] = offsets;
	strips[1] = counts;
	return nbrRowsPerStrip;
}

int[] formatColorMap(DWTRGB[] rgbs) {
	/* 
	* In a TIFF ColorMap, all red come first, followed by
	* green and blue. All values must be converted from
	* 8 bit to 16 bit. 
	*/
	int[] colorMap = new int[rgbs.length * 3];
	int offsetGreen = rgbs.length;
	int offsetBlue = rgbs.length * 2;
	for (int i = 0; i < rgbs.length; i++) {
		colorMap[i] = rgbs[i].red << 8 | rgbs[i].red;
		colorMap[i + offsetGreen] = rgbs[i].green << 8 | rgbs[i].green;
		colorMap[i + offsetBlue] = rgbs[i].blue << 8 | rgbs[i].blue;
	}
	return colorMap;
}

void parseEntries(byte[] buffer) { // throws IOException {
	for (int offset = 0; offset < buffer.length; offset += IFD_ENTRY_SIZE) {
		int tag = toInt(buffer, offset, TYPE_SHORT);
		int type = toInt(buffer, offset + 2, TYPE_SHORT);
		int count = toInt(buffer, offset + 4, TYPE_LONG);
		switch (tag) {
			case TAG_ImageWidth: {
				imageWidth = getEntryValue(type, buffer, offset);
				break;
			}
			case TAG_ImageLength: {
				imageLength = getEntryValue(type, buffer, offset);
				break;
			}
			case TAG_BitsPerSample: {
				if (type != TYPE_SHORT) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
				bitsPerSample = new int[count];
				getEntryValue(type, buffer, offset, bitsPerSample);
				break;
			}
			case TAG_Compression: {
				compression = getEntryValue(type, buffer, offset);
				break;
			}
			case TAG_PhotometricInterpretation: {
				photometricInterpretation = getEntryValue(type, buffer, offset);
				break;
			}
			case TAG_StripOffsets: {
				if (type != TYPE_LONG && type != TYPE_SHORT) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
				stripOffsets = new int[count];
				getEntryValue(type, buffer, offset, stripOffsets);
				break;
			}
			case TAG_SamplesPerPixel: {
				if (type != TYPE_SHORT) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
				samplesPerPixel = getEntryValue(type, buffer, offset);
				/* Only the basic 1 and 3 values are supported */
				if (samplesPerPixel != 1 && samplesPerPixel != 3){
					DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_DEPTH);
				}
				break;
			}
			case TAG_RowsPerStrip: {
				rowsPerStrip = getEntryValue(type, buffer, offset);
				break;
			}
			case TAG_StripByteCounts: {
				stripByteCounts = new int[count];
				getEntryValue(type, buffer, offset, stripByteCounts);
				break;
			}
			case TAG_XResolution: {
				/* Ignored */
				break;
			}
			case TAG_YResolution: {
				/* Ignored */
				break;
			}
			case TAG_T4Options: {
				if (type != TYPE_LONG) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
				t4Options = getEntryValue(type, buffer, offset);
				if ((t4Options & 0x1) == 1) {
					/* 2-dimensional coding is not supported */
					DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT);
				}
				break;
			}
			case TAG_ResolutionUnit: {
				/* Ignored */
				break;
			}
			case TAG_ColorMap: {
				if (type != TYPE_SHORT) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
				/* Get the offset of the colorMap (use TYPE_LONG) */
				colorMapOffset = getEntryValue(TYPE_LONG, buffer, offset);
				break;
			}
			default : break;
		}
	}
}

public ImageData read() { // throws IOException {
	/* Set TIFF default values */
	int[] intArr;
	intArr ~= 1;
	bitsPerSample = intArr;
	colorMapOffset = NO_VALUE;
	compression = 1;
	imageLength = NO_VALUE;
	imageWidth = NO_VALUE;
	photometricInterpretation = NO_VALUE;
	rowsPerStrip = Integer.MAX_VALUE;
	samplesPerPixel = 1;
	stripByteCounts = null;
	stripOffsets = null;
	
	byte[] buffer = new byte[2];
	file.read(buffer);
	int numberEntries = toInt(buffer, 0, TYPE_SHORT);
	buffer = new byte[IFD_ENTRY_SIZE * numberEntries];
	file.read(buffer);
	parseEntries(buffer);
	
	PaletteData palette = null;
	depth = 0;
	switch (photometricInterpretation) {
		case 0:
		case 1: {
			/* Bilevel or Grayscale image */
			palette = getGrayPalette();
			depth = bitsPerSample[0];
			break;
		}
		case 2: {
			/* RGB image */
			if (colorMapOffset != NO_VALUE) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
			/* SamplesPerPixel 3 is the only value supported */
			palette = getRGBPalette(bitsPerSample[0], bitsPerSample[1], bitsPerSample[2]);
			depth = bitsPerSample[0] + bitsPerSample[1] + bitsPerSample[2];
			break;		
		}
		case 3: {
			/* Palette Color image */
			if (colorMapOffset == NO_VALUE) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
			palette = getColorMap();
			depth = bitsPerSample[0];
			break;
		}
		default: {
			DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
		}
	}

	ImageData image = ImageData.internal_new(
			imageWidth,
			imageLength, 
			depth,
			palette,
			1,
			null,
			0,
			null,
			null,
			-1,
			-1,
			DWT.IMAGE_TIFF,
			0,
			0,
			0,
			0);
	decodePixels(image);
	return image;
}

int toInt(byte[] buffer, int i, int type) {
	if (type == TYPE_LONG) {
		return isLittleEndian ? 
		(buffer[i] & 0xFF) | ((buffer[i + 1] & 0xFF) << 8) | ((buffer[i + 2] & 0xFF) << 16) | ((buffer[i + 3] & 0xFF) << 24) :
		(buffer[i + 3] & 0xFF) | ((buffer[i + 2] & 0xFF) << 8) | ((buffer[i + 1] & 0xFF) << 16) | ((buffer[i] & 0xFF) << 24);
	}
	if (type == TYPE_SHORT) {
		return isLittleEndian ? 
		(buffer[i] & 0xFF) | ((buffer[i + 1] & 0xFF) << 8) :
		(buffer[i + 1] & 0xFF) | ((buffer[i] & 0xFF) << 8);		
	}
	/* Invalid type */
	DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
	return -1;
}

void write(int photometricInterpretation) { // throws IOException {
	boolean isRGB = photometricInterpretation == 2;
	boolean isColorMap = photometricInterpretation == 3;
	boolean isBiLevel = photometricInterpretation == 0 || photometricInterpretation == 1;

	int imageWidth = image.width;
	int imageLength = image.height;
	int rowByteSize = image.bytesPerLine;
	
	int numberEntries = isBiLevel ? 9 : 11;
	int lengthDirectory = 2 + 12 * numberEntries + 4;
	/* Offset following the header and the directory */
	int nextOffset = 8 + lengthDirectory;

	/* Extra space used by XResolution and YResolution values */
	int extraBytes = 16;

	int[] colorMap = null;
	if (isColorMap) {	
		PaletteData palette = image.palette;
		DWTRGB[] rgbs = palette.getRGBs();
		colorMap = formatColorMap(rgbs);
		/* The number of entries of the Color Map must match the bitsPerSample field */
		if (colorMap.length != 3 * 1 << image.depth) DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT);
		/* Extra space used by ColorMap values */
		extraBytes += colorMap.length * 2;
	}
	if (isRGB) {
		/* Extra space used by BitsPerSample values */
		extraBytes += 6;
	} 
	/* TIFF recommends storing the data in strips of no more than 8 Ko */
	byte[] data = image.data;
	int[][] strips = new int[][2];
	int nbrRowsPerStrip = formatStrips(rowByteSize, imageLength, data, 8192, nextOffset, extraBytes, strips);
	int[] stripOffsets = strips[0];
	int[] stripByteCounts = strips[1];

	int bitsPerSampleOffset = NO_VALUE;
	if (isRGB) {
		bitsPerSampleOffset = nextOffset;
		nextOffset += 6;
	}
	int stripOffsetsOffset = NO_VALUE, stripByteCountsOffset = NO_VALUE;
	int xResolutionOffset, yResolutionOffset, colorMapOffset = NO_VALUE;
	int cnt = stripOffsets.length;
	if (cnt > 1) {
		stripOffsetsOffset = nextOffset;
		nextOffset += 4 * cnt;
		stripByteCountsOffset = nextOffset;
		nextOffset += 4 * cnt;
	}
	xResolutionOffset = nextOffset;
	nextOffset += 8;
	yResolutionOffset = nextOffset;
	nextOffset += 8;
	if (isColorMap) {
		colorMapOffset = nextOffset;
		nextOffset += colorMap.length * 2;
	}
	/* TIFF header */
	writeHeader();
	
	/* Image File Directory */
	out_renamed.writeShort(numberEntries);	
	writeEntry(TAG_ImageWidth, TYPE_LONG, 1, imageWidth);	
	writeEntry(TAG_ImageLength, TYPE_LONG, 1, imageLength);
	if (isColorMap) writeEntry(TAG_BitsPerSample, TYPE_SHORT, 1, image.depth);
	if (isRGB) writeEntry(TAG_BitsPerSample, TYPE_SHORT, 3, bitsPerSampleOffset);
	writeEntry(TAG_Compression, TYPE_SHORT, 1, COMPRESSION_NONE);
	writeEntry(TAG_PhotometricInterpretation, TYPE_SHORT, 1, photometricInterpretation);
	writeEntry(TAG_StripOffsets, TYPE_LONG, cnt, cnt > 1 ? stripOffsetsOffset : stripOffsets[0]);
	if (isRGB) writeEntry(TAG_SamplesPerPixel, TYPE_SHORT, 1, 3);
	writeEntry(TAG_RowsPerStrip, TYPE_LONG, 1, nbrRowsPerStrip);
	writeEntry(TAG_StripByteCounts, TYPE_LONG, cnt, cnt > 1 ? stripByteCountsOffset : stripByteCounts[0]);
	writeEntry(TAG_XResolution, TYPE_RATIONAL, 1, xResolutionOffset);
	writeEntry(TAG_YResolution, TYPE_RATIONAL, 1, yResolutionOffset);
	if (isColorMap) writeEntry(TAG_ColorMap, TYPE_SHORT, colorMap.length, colorMapOffset);
	/* Offset of next IFD (0 for last IFD) */
	out_renamed.writeInt(0);
	
	/* Values longer than 4 bytes Section */
	
	/* BitsPerSample 8,8,8 */
	if (isRGB) for (int i = 0; i < 3; i++) out_renamed.writeShort(8);
	if (cnt > 1) {
		for (int i = 0; i < cnt; i++) out_renamed.writeInt(stripOffsets[i]);
		for (int i = 0; i < cnt; i++) out_renamed.writeInt(stripByteCounts[i]);
	}
	/* XResolution and YResolution set to 300 dpi */
	for (int i = 0; i < 2; i++) {
		out_renamed.writeInt(300);
		out_renamed.writeInt(1);
	}
	/* ColorMap */
	if (isColorMap) for (int i = 0; i < colorMap.length; i++) out_renamed.writeShort(colorMap[i]);
	
	/* Image Data */
	out_renamed.write(data);
}

void writeEntry(short tag, int type, int count, int value) { // throws IOException {
	out_renamed.writeShort(tag);
	out_renamed.writeShort(type);
	out_renamed.writeInt(count);
	out_renamed.writeInt(value);
}

void writeHeader() { // throws IOException {
	/* little endian */
	out_renamed.writeByte(cast(byte)0x49);
	out_renamed.writeByte(cast(byte)0x49);

	/* TIFF identifier */
	out_renamed.writeShort(42);
	/* 
	* Offset of the first IFD is chosen to be 8.
	* It is word aligned and immediately after this header.
	*/
	out_renamed.writeInt(8);
}

void writeToStream(LEDataOutputStream byteStream) { // throws IOException {
	out_renamed = byteStream;
	int photometricInterpretation = -1;
	
	/* Scanline pad must be 1 */
	if (image.scanlinePad != 1) DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT);
	switch (image.depth) {
		case 1: {
			/* Palette must be black and white or white and black */
			PaletteData palette = image.palette;
			DWTRGB[] rgbs = palette.colors;
			if (palette.isDirect || rgbs is null || rgbs.length != 2) DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT);
			DWTRGB rgb0 = rgbs[0];
			DWTRGB rgb1 = rgbs[1];
			if (!(rgb0.red == rgb0.green && rgb0.green == rgb0.blue &&
				rgb1.red == rgb1.green && rgb1.green == rgb1.blue &&
				((rgb0.red == 0x0 && rgb1.red == 0xFF) || (rgb0.red == 0xFF && rgb1.red == 0x0)))) {
				DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT); 
			}
			/* 0 means a color index of 0 is imaged as white */
			photometricInterpretation = image.palette.colors[0].red == 0xFF ? 0 : 1;
			break;
		}
		case 4:
		case 8: {
			photometricInterpretation = 3;
			break;
		}
		case 24: {
			photometricInterpretation = 2;
			break;
		}
		default: {
			DWT.error(__FILE__, __LINE__, DWT.ERROR_UNSUPPORTED_FORMAT);
		}
	}
	write(photometricInterpretation);
}

}





/*
* Decoder for 
* - CCITT Group 3 1-Dimensional Modified Huffman run length encoding
*   (TIFF compression type 2)
* - CCITT T.4 bi-level encoding 1D
*   (TIFF compression type 3 option 1D) 
*/
class TIFFModifiedHuffmanCodec {
	static short[][][] BLACK_CODE = [
		/* 2 bits  */
		[[2, 3], [3, 2]],
		/* 3 bits  */
		[[2, 1], [3, 4]],
		/* 4 bits  */
		[[2, 6], [3, 5]],
		/* 5 bits  */
		[[3, 7]],
		/* 6 bits  */
		[[4, 9], [5, 8]],
		/* 7 bits  */
		[[4, 10], [5, 11], [7, 12]],
		/* 8 bits  */
		[[4, 13], [7, 14]],
		/* 9 bits  */
		[[24, 15]],
		/* 10 bits */
		[[8, 18], [15, 64], [23, 16], [24, 17], [55, 0]],
		/* 11 bits */
		[/* EOL */[0, -1], [8, 1792], [23, 24], [24, 25], [40, 23], [55, 22], [103, 19],
		[104, 20], [108, 21], [12, 1856], [13, 1920]],
		/* 12 bits */
		[[18, 1984], [19, 2048], [20, 2112], [21, 2176], [22, 2240], [23, 2304],
		[28, 2368], [29, 2432], [30, 2496], [31, 2560], [36, 52], [39, 55], [40, 56],
		[43, 59], [44, 60], [51, 320], [52, 384], [53, 448], [55, 53], [56, 54], [82, 50],
		[83, 51], [84, 44], [85, 45], [86, 46], [87, 47], [88, 57], [89, 58], [90, 61],
		[91, 256], [100, 48], [101, 49], [102, 62], [103, 63], [104, 30], [105, 31],
		[106, 32], [107, 33], [108, 40], [109, 41], [200, 128], [201, 192], [202, 26],
		[203, 27], [204, 28], [205, 29], [210, 34], [211, 35], [212, 36], [213, 37],
		[214, 38], [215, 39], [218, 42], [219, 43]],
		/* 13 bits */
		[[74, 640], [75, 704], [76, 768], [77, 832], [82, 1280], [83, 1344], [84, 1408],
		[85, 1472], [90, 1536], [91, 1600], [100, 1664], [101, 1728], [108, 512],
		[109, 576], [114, 896], [115, 960], [116, 1024], [117, 1088], [118, 1152],
		[119, 1216]]
	];

	static short[][][] WHITE_CODE = [
		/* 4 bits */
		[[7, 2], [8, 3], [11, 4], [12, 5], [14, 6], [15, 7]],
		/* 5 bits */
		[[7, 10], [8, 11], [18, 128], [19, 8], [20, 9], [27, 64]],
		/* 6 bits */
		[[3, 13], [7, 1], [8, 12], [23, 192], [24, 1664], [42, 16], [43, 17], [52, 14],
		[53, 15]],
		/* 7 bits */
		[[3, 22], [4, 23], [8, 20], [12, 19], [19, 26], [23, 21], [24, 28], [36, 27],
		[39, 18], [40, 24], [43, 25], [55, 256]],
		/* 8 bits */
		[[2, 29], [3, 30], [4, 45], [5, 46], [10, 47], [11, 48], [18, 33], [19, 34],
		[20, 35], [21, 36], [22, 37], [23, 38], [26, 31], [27, 32], [36, 53], [37, 54],
		[40, 39], [41, 40], [42, 41], [43, 42], [44, 43], [45, 44], [50, 61], [51, 62],
		[52, 63], [53, 0], [54, 320], [55, 384], [74, 59], [75, 60], [82, 49], [83, 50],
		[84, 51], [85, 52], [88, 55], [89, 56], [90, 57], [91, 58], [100, 448],
		[101, 512], [103, 640], [104, 576]],
		/* 9 bits */
		[[152, 1472], [153, 1536], [154, 1600], [155, 1728], [204, 704], [205, 768],
		[210, 832], [211, 896], [212, 960], [213, 1024], [214, 1088], [215, 1152],
		[216, 1216], [217, 1280], [218, 1344], [219, 1408]],
		/* 10 bits */
		[],
		/* 11 bits */
		[[8, 1792], [12, 1856], [13, 1920]],
		/* 12 bits */
		[/* EOL */[1, -1], [18, 1984], [19, 2048], [20, 2112], [21, 2176], [22, 2240], [23, 2304],
		[28, 2368], [29, 2432], [30, 2496], [31, 2560]]
	];
	
	static int BLACK_MIN_BITS = 2;
	static int WHITE_MIN_BITS = 4;

	boolean isWhite;
	int whiteValue = 0;
	int blackValue = 1;
	byte[] src;
	byte[] dest;
	int byteOffsetSrc = 0;
	int bitOffsetSrc = 0;
	int byteOffsetDest = 0;
	int bitOffsetDest = 0;
	int code = 0;
	int nbrBits = 0;
	/* nbr of bytes per row */
	int rowSize;

public int decode(byte[] src, byte[] dest, int offsetDest, int rowSize, int nRows) {
	this.src = src;
	this.dest = dest;
	this.rowSize = rowSize;
	byteOffsetSrc = 0;
	bitOffsetSrc = 0;
	byteOffsetDest = offsetDest;
	bitOffsetDest = 0;
	int cnt = 0;
	while (cnt < nRows && decodeRow()) {
		cnt++;
		/* ubyte align_renameded */
		if (bitOffsetDest > 0) {
			byteOffsetDest++;
			bitOffsetDest = 0; 
		}
	}
	return byteOffsetDest - offsetDest;
}

boolean decodeRow() {
	isWhite = true;
	int n = 0;
	while (n < rowSize) {
		int runLength = decodeRunLength();
		if (runLength < 0) return false;
		n += runLength;
		setNextBits(isWhite ? whiteValue : blackValue, runLength);
		isWhite = !isWhite;
	}
	return true;
}

int decodeRunLength() {
	int runLength = 0;
	int partialRun = 0;
	short[][][] huffmanCode = isWhite ? WHITE_CODE : BLACK_CODE;
	while (true) {
		boolean found = false;
		nbrBits = isWhite ? WHITE_MIN_BITS : BLACK_MIN_BITS;
		code = getNextBits(nbrBits);
		for (int i = 0; i < huffmanCode.length; i++) {
			for (int j = 0; j < huffmanCode[i].length; j++) {
				if (huffmanCode[i][j][0] == code) {
					found = true;
					partialRun = huffmanCode[i][j][1];
					if (partialRun == -1) {
						/* Stop when reaching EOL on last ubyte */
						if (byteOffsetSrc == src.length - 1) return -1;
						/* Group 3 starts each row with an EOL - ignore it */
					} else {
						runLength += partialRun;
						if (partialRun < 64) return runLength;
					}
					break;
				}
			}
			if (found) break;
			code = code << 1 | getNextBit();
		}
		if (!found) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);			 
	}
	return 0;
}

int getNextBit() {
	int value = (src[byteOffsetSrc] >>> (7 - bitOffsetSrc)) & 0x1;
	bitOffsetSrc++;
	if (bitOffsetSrc > 7) {
		byteOffsetSrc++;
		bitOffsetSrc = 0;
	}
	return value;
}

int getNextBits(int cnt) {
	int value = 0;
	for (int i = 0; i < cnt; i++) {
		value = value << 1 | getNextBit();
	}
	return value;
}

void setNextBits(int value, int cnt) {
	int n = cnt;
	while (bitOffsetDest > 0 && bitOffsetDest <= 7 && n > 0) {
		dest[byteOffsetDest] = value == 1 ?
			cast(byte)(dest[byteOffsetDest] | (1 << (7 - bitOffsetDest))) :
			cast(byte)(dest[byteOffsetDest] & ~(1 << (7 - bitOffsetDest)));
		n--;
		bitOffsetDest++; 
	}
	if (bitOffsetDest == 8) {
		byteOffsetDest++;
		bitOffsetDest = 0;
	}
	while (n >= 8) {
		dest[byteOffsetDest++] = cast(byte) (value == 1 ? 0xFF : 0);
		n -= 8;
	}
	while (n > 0) {
		dest[byteOffsetDest] = value == 1 ?
			cast(byte)(dest[byteOffsetDest] | (1 << (7 - bitOffsetDest))) :
			cast(byte)(dest[byteOffsetDest] & ~(1 << (7 - bitOffsetDest)));
		n--;
		bitOffsetDest++;		
	}	
}

}




class TIFFRandomFileAccess {

	LEDataInputStream inputStream;
	int start, current, next;
	byte[][] buffers;

	static int CHUNK_SIZE = 8192;
	static int LIST_SIZE = 128;

public this(LEDataInputStream stream) {
	inputStream = stream;
	start = current = next = cast(int)inputStream.getPosition();
	buffers = new byte[][LIST_SIZE];
}

void seek(int pos) { // throws IOException {
	if (pos == current) return;
	if (pos < start) throw new Exception("StreamException");
	current = pos;	
	if (current > next) {
		int n = current - next;
		/* store required bytes */
		int index = next / CHUNK_SIZE;
		int offset = next % CHUNK_SIZE;
		while (n > 0) {
			if (index >= buffers.length) {
				byte[][] oldBuffers = buffers;
				int len = oldBuffers.length;
				buffers = new byte[][Math.max(index + 1, len + LIST_SIZE)];
				System.arraycopy(oldBuffers, 0u, buffers, 0u, oldBuffers.length);
			}
			if (buffers[index] is null) buffers[index] = new byte[CHUNK_SIZE];
			int cnt = inputStream.readBytes(buffers[index], offset, Math.min(n, CHUNK_SIZE - offset));
			n -= cnt;
			next += cnt;
			index++;
			offset = 0;
		}
	}
}

void read(byte[] b) { // throws IOException {
	int size = b.length;
	int nCached = Math.min(size, next - current);
	int nMissing = size - next + current;
	int destNext = 0;
	if (nCached > 0) {
		/* Get cached bytes */
		int index = current / CHUNK_SIZE;
		int offset = current % CHUNK_SIZE;		
		while (nCached > 0) {
			int cnt = Math.min(nCached, CHUNK_SIZE - offset);
			System.arraycopy(buffers[index], offset, b, destNext, cnt);
			nCached -= cnt; 
			destNext += cnt;
			index++;
			offset = 0;
		}
	}
	if (nMissing > 0) {
		/* Read required bytes */
		int index = next / CHUNK_SIZE;
		int offset = next % CHUNK_SIZE;
		while (nMissing > 0) {
			if (index >= buffers.length) {
				byte[][] oldBuffers = buffers;
				buffers = new byte[][Math.max(index, cast(int)(oldBuffers.length + LIST_SIZE))];
				System.arraycopy(oldBuffers, 0, buffers, 0, oldBuffers.length);
			}
			if (buffers[index] is null) buffers[index] = new byte[CHUNK_SIZE];
			int cnt = inputStream.readBytes(buffers[index], offset, Math.min(nMissing, CHUNK_SIZE - offset));
			System.arraycopy(buffers[index], offset, b, destNext, cnt);
			nMissing -= cnt;
			next += cnt;
			destNext += cnt;
			index++;
			offset = 0;
		}
	}
	current += size;
}

}







/**
 * Baseline TIFF decoder revision 6.0
 * Extension T4-encoding CCITT T.4 1D
 */
class TIFFFileFormat : FileFormat {

boolean isFileFormat(LEDataInputStream stream) {
	try {
		byte[] header = new byte[4];
		stream.read(header);
		stream.unread(header);
		if (header[0] != header[1]) return false;
		if (!(header[0] == 0x49 && header[2] == 42 && header[3] == 0) &&
			!(header[0] == 0x4d && header[2] == 0 && header[3] == 42)) {
			return false;
		} 	
		return true;
	} catch (Exception e) {
		return false;
	}
}

ImageData[] loadFromByteStream() {	
	byte[] header = new byte[8];
	boolean isLittleEndian;
	ImageData[] images;
	TIFFRandomFileAccess file = new TIFFRandomFileAccess(inputStream);
	try {
		file.read(header);
		if (header[0] != header[1]) DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
		if (!(header[0] == 0x49 && header[2] == 42 && header[3] == 0) &&
			!(header[0] == 0x4d && header[2] == 0 && header[3] == 42)) {
			DWT.error(__FILE__, __LINE__, DWT.ERROR_INVALID_IMAGE);
		} 
		isLittleEndian = header[0] == 0x49;	
		int offset = isLittleEndian ? 
			(header[4] & 0xFF) | ((header[5] & 0xFF) << 8) | ((header[6] & 0xFF) << 16) | ((header[7] & 0xFF) << 24) :
			(header[7] & 0xFF) | ((header[6] & 0xFF) << 8) | ((header[5] & 0xFF) << 16) | ((header[4] & 0xFF) << 24);
		file.seek(offset);
		TIFFDirectory directory = new TIFFDirectory(file, isLittleEndian, loader);
		ImageData image = directory.read();
		/* A baseline reader is only expected to read the first directory */
		ImageData[] ImageDataArr;
		ImageDataArr ~= image;
		images = ImageDataArr;
	} catch (StreamException e) {
		DWT.error(__FILE__, __LINE__,  e);
	}
	return images;
}

void unloadIntoByteStream(ImageData image) {
	TIFFDirectory directory = new TIFFDirectory(image);
	try {
		directory.writeToStream(outputStream);
	} catch (StreamException e) {
		DWT.error(__FILE__, __LINE__,  e);
	}
}

}
